/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

package au.com.lastweekend.jim.imagebase;

import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import javax.imageio.ImageIO;

import org.apache.log4j.Logger;

import au.com.lastweekend.jim.imagebase.query.QueryCondition;
import au.com.lastweekend.jim.io.exif.ThumbnailTiffDirectory;
import au.com.lastweekend.jim.io.exif.TiffDirectory;
import au.com.lastweekend.jim.io.jpeg.JpegProcessingException;
import au.com.lastweekend.jim.io.jpeg.JpegReader;
import au.com.lastweekend.jim.io.jpeg.JpegWriter;
import au.com.lastweekend.jim.io.jpeg.read.AllSegmentsReaderRegistry;
import au.com.lastweekend.jim.io.jpeg.read.ExifSegmentReaderRegistry;
import au.com.lastweekend.jim.io.jpeg.write.DefaultDirectoryWriterRegistry;
import au.com.lastweekend.jim.io.jpeg.write.JpegDirectoryWriterRegistry;
import au.com.lastweekend.jim.io.jpeg.write.RotateAntiClockwiseDirectoryWriterRegistry;
import au.com.lastweekend.jim.io.jpeg.write.RotateClockwiseDirectoryWriterRegistry;
import au.com.lastweekend.jim.io.metadata.Metadata;
import au.com.lastweekend.jim.util.FileOps;
import au.com.lastweekend.jim.util.ImageUtils;

/**
 * @author grant@lastweekend.com.au
 * @version $Id: ImageManager.java,v 1.11 2006/04/16 09:15:38 ggardner Exp $
 */
public class ImageManager {

    public enum Rotation {
        CLOCKWISE(new RotateClockwiseDirectoryWriterRegistry(), au.com.lastweekend.jim.io.exif.ExifDirectory.Rotation.CLOCKWISE),
        ANTI_CLOCKWISE(new RotateAntiClockwiseDirectoryWriterRegistry(),
                au.com.lastweekend.jim.io.exif.ExifDirectory.Rotation.ANTI_CLOCKWISE);

        private au.com.lastweekend.jim.io.exif.ExifDirectory.Rotation exifRotation;
        private JpegDirectoryWriterRegistry writerRegistry;

        private Rotation(JpegDirectoryWriterRegistry writerRegistry,
                au.com.lastweekend.jim.io.exif.ExifDirectory.Rotation exifRotation) {

            this.exifRotation = exifRotation;
            this.writerRegistry = writerRegistry;
        }

    }

    private static final File TEMP_DIR = new File(System.getProperty("java.io.tmpdir"));
    private static final Logger LOG = Logger.getLogger(ImageManager.class);

    private ImageBase imageBase;
    private SoftHashMap<File, ImageInfo> imageInfoMap = new SoftHashMap<File, ImageInfo>();

    private ExternalImporter importer;

    public ImageManager(ImageBase imageBase, ExternalImporter importer) {

        this.imageBase = imageBase;
        this.importer = importer;

    }

    /**
     * @deprecated use QueryCondition instead.
     * @param searchString
     * @return an ImageInfo collection
     */
    @Deprecated
    public Collection<ImageInfo> search(String searchString) {

        Collection<ImageInfo> imageInfoList = new HashSet<ImageInfo>();
        LOG.info("Searching:" + searchString);
        Collection<ImageBaseDirectory> imageDirectories = getImageBase().search(searchString);
        for (Iterator<ImageBaseDirectory> iter = imageDirectories.iterator(); iter.hasNext();) {
            ImageBaseDirectory imageDirectory = iter.next();

            imageInfoList.addAll(getImageInfos(imageDirectory));
        }
        return imageInfoList;
    }

    public Collection<ImageInfo> search(QueryCondition condition) {

        Collection<ImageInfo> imageInfoList = new HashSet<ImageInfo>();
        LOG.info("Searching:" + condition);
        Collection<ImageBaseDirectory> imageDirectories = getImageBase().search(condition);
        for (Iterator<ImageBaseDirectory> iter = imageDirectories.iterator(); iter.hasNext();) {
            ImageBaseDirectory imageDirectory = iter.next();

            imageInfoList.addAll(getImageInfos(imageDirectory));
        }
        return imageInfoList;

    }

    private Collection<ImageInfo> getImageInfos(ImageBaseDirectory imageDirectory) {

        Collection<ImageInfo> imageInfos = new HashSet<ImageInfo>();
        Collection<File> files = imageDirectory.getFiles();

        for (Iterator<File> iter = files.iterator(); iter.hasNext();) {
            File fileKey = iter.next();

            if (fileKey.exists()) {
                imageInfos.add(getImageInfo(fileKey));
            } else {
                LOG.warn("Previously known path " + fileKey + " not available, file may have been moved or deleted");
            }
        }
        if (imageInfos.isEmpty()) {
            ImageInfo lostImageInfo = new ImageInfo(getImageBase(), new File(imageDirectory.getFilename()), imageDirectory);
            imageInfos.add(lostImageInfo);
        }
        return imageInfos;

    }

    private ImageInfo getImageInfo(File file) {

        ImageInfo imageInfo = imageInfoMap.get(file);
        if (imageInfo == null) {
            imageInfo = new ImageInfo(getImageBase(), file);
            imageInfoMap.put(file, imageInfo);
        }
        return imageInfo;

    }

    public Collection<ImageInfo> getDirectoryImages(File dirFile) {

        cleanupDirectoryPaths(dirFile);
        File[] dirArray = dirFile.listFiles(new FilenameFilter() {

            public boolean accept(File dir, String name) {

                return name.toLowerCase().endsWith(".jpg");
            }

        });

        if (dirArray != null) {
            List<ImageInfo> imageList = new ArrayList<ImageInfo>();
            for (int i = 0; i < dirArray.length; i++) {
                File file = dirArray[i];
                imageList.add(getImageInfo(file));
            }
            return imageList;
        }
        return Collections.emptyList();

    }

    private void cleanupDirectoryPaths(File dirFile) {

        dirFile = ImageBaseDirectory.convertToImageBaseFile(dirFile);
        Collection<ImageBaseDirectory> results = getImageBase().search(
                getImageBase().getEqualsCondition("path", dirFile.getPath()));
        for (ImageBaseDirectory imageDirectory : results) {
            File expectedFile = new File(dirFile, imageDirectory.getFilename());
            if (!expectedFile.exists()) {
                LOG.info("Previously known image file " + expectedFile + " has disappeared. Removing location from imagebase");
                imageDirectory.removeFile(expectedFile);
            }
        }
    }

    public ImageBase getImageBase() {

        return imageBase;
    }

    public void importToImageBase(ImageInfo imageInfo) {

        importToImageBase(imageInfo.getFile());
        imageInfo.reset();
    }

    /**
     * @param file
     * 
     */
    private void importToImageBase(File file) {

        String filename = file.getName();
        boolean isNewEntry = false;
        ImageBaseDirectory imageBaseDirectory = getImageBase().findDirectory(filename);
        if (imageBaseDirectory == null) {
            imageBaseDirectory = new ImageBaseDirectory();
            imageBaseDirectory.setFilename(filename);
            isNewEntry = true;
        } else if (LOG.isDebugEnabled()) {
            LOG.debug("Concatenating import with existing entry for " + filename);
        }

        if (file.exists()) {
            imageBaseDirectory.addFile(file);
        }

        importer.importExternalMetadata(file, imageBaseDirectory);

        if (isNewEntry) {
            getImageBase().storeDirectory(imageBaseDirectory);
        }

        for (String keyword : imageBaseDirectory.getKeywords()) {
            getImageBase().addKeyword(keyword);
        }

        LOG.info("Imported " + file.getAbsolutePath());

    }

    /**
     * Create new thumbnails.
     * 
     */
    public void injectThumbnail(ImageInfo imageInfo) {

        final File imageFile = imageInfo.getFile();
        BufferedImage image;

        try {
            image = ImageIO.read(imageFile);
        } catch (IOException e) {
            LOG.warn("Unable to read image " + imageFile.getPath(), e);
            return;
        }

        double iWidth = image.getWidth();
        double iHeight = image.getHeight();
        double cWidth = 160.0;
        double cHeight = 160.0;

        double scale = ImageUtils.getScaleToFit(iWidth, iHeight, cWidth, cHeight);
        cWidth = iWidth * scale;
        cHeight = iHeight * scale;

        final BufferedImage thumbnailImage = new BufferedImage((int) cWidth, (int) cHeight, image.getType());

        Image scaledImage = image.getScaledInstance((int) cWidth, (int) cHeight, Image.SCALE_SMOOTH);
        thumbnailImage.createGraphics().drawImage(scaledImage, 0, 0, null);
        writeThumbnail(imageInfo, thumbnailImage);

        LOG.info("Injected thumbnail to " + imageFile);
    }

    public void rotate(ImageInfo imageInfo, final Rotation direction, boolean rotateByTag) {

        if (rotateByTag) {
            rotateOrientation(imageInfo, direction);
        } else {
            rotate(imageInfo, direction);
        }

    }

    /**
     * @param imageInfo
     * @param direction
     */
    private void rotate(ImageInfo imageInfo, Rotation direction) {

        File imageFile = imageInfo.getFile();
        try {
            JpegReader reader = new JpegReader(new AllSegmentsReaderRegistry());
            Metadata metadata = reader.readTransformableImage(imageFile);

            writeImage(metadata, imageFile, direction.writerRegistry);
            LOG.info("Rotated " + direction + " " + imageFile);
        } catch (JpegProcessingException e) {
            LOG.error("Rotation failed", e);
        } catch (IOException e) {
            LOG.error("Rotation failed", e);
        } finally {
            imageInfo.reset();
        }

    }

    /**
     * @param imageInfo
     * @param direction
     */
    private void rotateOrientation(ImageInfo imageInfo, Rotation direction) {

        File imageFile = imageInfo.getFile();
        try {
            JpegReader reader = new JpegReader(new ExifSegmentReaderRegistry());
            Metadata metadata = reader.readImage(imageFile);

            TiffDirectory tiffDir = (TiffDirectory) metadata.getDirectory(TiffDirectory.class);

            tiffDir.rotate(direction.exifRotation);

            if (metadata.containsDirectory(ThumbnailTiffDirectory.class)) {
                ThumbnailTiffDirectory thumbnailDir = (ThumbnailTiffDirectory) metadata.getDirectory(ThumbnailTiffDirectory.class);
                if (thumbnailDir.hasAutoRotationAngle()) {
                    thumbnailDir.rotate(direction.exifRotation);
                }

            }

            writeImage(metadata, imageFile, new DefaultDirectoryWriterRegistry());
            LOG.info("Rotated by orientation " + direction + " " + imageFile);
        } catch (Exception e) {
            LOG.error("Rotation failed", e);
        } finally {
            imageInfo.reset();
        }

    }

    /**
     * 
     * @param huntDirectory
     *            Root directory to search for images within.
     * @param selectedImages
     *            Set of ImageInfo's to hunt for
     */
    public void huntForFiles(File huntDirectory, Collection<ImageInfo> selectedImages) {

        Collection<String> fileNames = new HashSet<String>();

        for (ImageInfo imageInfo : selectedImages) {

            if (imageInfo.hasImageBaseDirectory()) {
                fileNames.add(imageInfo.getFile().getName());
            }
        }
        if (!fileNames.isEmpty()) {
            FileFilter filter = new FilenamesFilter(fileNames);
            filter = new DirectoryPlusFilter(filter);

            Collection<File> files = FileOps.INSTANCE.find(huntDirectory, filter);
            LOG.info("Found " + files.size() + " matching files");
            for (File file : files) {

                ImageBaseDirectory imageDirectory = imageBase.findDirectory(file.getName());
                imageDirectory.addFile(file);
            }
        } else {
            LOG.info("No imported files selected");
        }
    }

    private void writeImage(Metadata metadata, File imageFile, final JpegDirectoryWriterRegistry writerRegistry)
            throws IOException, JpegProcessingException {

        // Get writeFile as temporary file
        File writeFile = File.createTempFile("jim", ".tmp", TEMP_DIR);

        JpegWriter writer = new JpegWriter(writerRegistry);
        // Write to temp file
        writer.writeImage(metadata, writeFile);

        // Backup file
        File backupFile = new File(imageFile.getParentFile(), imageFile.getName() + ".bak");
        if (!backupFile.exists()) {
            FileOps.INSTANCE.move(imageFile, backupFile);
        }

        // Move temp to real file
        FileOps.INSTANCE.move(writeFile, imageFile);
    }

    private void writeThumbnail(ImageInfo imageInfo, BufferedImage thumbnailImage) {

        ByteArrayOutputStream os = new ByteArrayOutputStream();
        File imageFile = imageInfo.getFile();

        try {
            ImageIO.write(thumbnailImage, "jpeg", os);

            ThumbnailTiffDirectory thumbnailDirectory = new ThumbnailTiffDirectory(os.toByteArray());

            JpegReader reader = new JpegReader(new ExifSegmentReaderRegistry());
            Metadata metadata = reader.readImage(imageFile);
            TiffDirectory tiffDirectory;

            if (metadata.containsDirectory(TiffDirectory.class)) {
                tiffDirectory = (TiffDirectory) metadata.getDirectory(TiffDirectory.class);
            } else {
                LOG.debug("No TiffDirectory, adding...");
                tiffDirectory = new TiffDirectory();
                metadata.insertDirectory(0, tiffDirectory);
            }

            LOG.debug("Adding new thumbnailDirectory");
            tiffDirectory.setLinkedDirectory(thumbnailDirectory);

            writeImage(metadata, imageFile, new DefaultDirectoryWriterRegistry());
        } catch (Exception e) {
            LOG.warn("Unable to inject thumbnail for " + imageFile.getPath(), e);
        } finally {
            imageInfo.reset();
        }
    }

}